package com.xxx.nio.longmessage;

import com.xxx.nio.utils.ByteBufferUtil;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * 超长消息的ByteBuffer扩容 Server端
 * 解决思路：
       1.  首先将 channel.read(buffer) 里，这个 buffer 局部变量 改为全局变量 (方便一次性读取 客户端数据)
       2.  在每个客户端SocketChannel 注册Selector时，第三参数 附件里 加入 开辟的内存ByteBuffer buffer (这时的 buffer 的生命周期 将和SelectionKey 一样了)
       3.  然后在 每次读事件 里 attachment获取这个附件参数强转回 ByteBuffer
       4.  子方法compact 结束后，主方法 对比position==limit检查是否超出内存，超出内存说明当前没有读取到将 触发读事件再次循环
       5.  如果超出内存：开辟原内存字节长度 改为两倍，进入下一次循环继续判断，直到满足长度 处理完
 */
@Slf4j
public class Server {

    private static void split(ByteBuffer source) {
        System.out.println("before split =============================================");
        // 读模式
        source.flip();
        for (int i = 0; i < source.limit(); i++) {
            byte b = source.get(i);
            if (b == '\n') {
                int position = source.position();
                int length = i + 1 - position;

                // 完整 消息 存入 新 buffer
                ByteBuffer target = ByteBuffer.allocate(length);
                // 从 source 读 ，写入 target
                for (int j = 0; j < length; j++) {
                    target.put(source.get()); // 每一次 get 时， position++
                    System.out.print(source.position() + ",");
                }
                System.out.println("]");
                ByteBufferUtil.debugAll(target);
            }
        }

        // 未读完部分向前压缩
        source.compact(); // 首次 read 1234567890abcdef 进入时，compact后， position=limit=16
        System.out.println("after split =============================================");

    }

    public static void main(String[] args) throws IOException {
        // 0.创建一个selector，用来管理多个channel
        Selector selector = Selector.open();

        // 1.创建Server端的SocketChannel，并将其注册到Selector上
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);  // 配置为非阻塞模式
        SelectionKey sscKey = ssc.register(selector, 0, null);// 将Server端的channel注册到Selector上，返回注册结果key
        // 注册完成后，监听感兴趣的事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);    // 对于Server端，监听accept()事件
        // 总共有4种类型的事件：
        //  ①accept()-Server端接受来自Client端的连接请求
        //  ②connect()-Client端发起向Server端的连接请求
        //  ③read()：可读事件
        //  ④write()：可写事件
        log.debug("register sscKey:{}", sscKey);

        // 2.绑定Server端监听端口
        ssc.bind(new InetSocketAddress(8080));

        while (true) {
            // 3.select()方法会一直阻塞，直到监听的事件到来
            selector.select();  // 阻塞
            // 4.当有事件到来，处理事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();  // selectedKeys包含了内部所有的事件
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                log.debug("key:{}", key);

                // 处理前面注册的监听事件
                // 根据不同的事件类型，分别进行处理
                if(key.isAcceptable()) {  // Server端的accept()事件
                    ssc = (ServerSocketChannel) key.channel();
                    SocketChannel sc = ssc.accept();  // 监听的是accept()事件
                    sc.configureBlocking(false);    // 配置为非阻塞模式
                    log.debug("sc:{}", sc);
                    // 分配 16字节的一个内存 来 存放接收的数据，作为附件和sc一起注册到selector
                    ByteBuffer buffer = ByteBuffer.allocate(16);  // 超长消息处理措施①
                    // 将SocketChannel也注册到Selector
                    SelectionKey scKey = sc.register(selector, 0, buffer);
                    // 监听感兴趣的事件
                    scKey.interestOps(SelectionKey.OP_READ);
                    log.debug("register scKey:{}", scKey);
                } else if (key.isReadable()) { // 监听的是读取数据事件
                    try {
                        SocketChannel sc = (SocketChannel) key.channel();
                        // 获取accept()事件的内存附件attachment
                        ByteBuffer buffer = (ByteBuffer) key.attachment();  // 超长消息处理措施②
                        int read = sc.read(buffer);
                        if(read == -1) {  // read()返回-1表示客户端正常断开连接
                            log.debug("client close channel. sc:{}", sc);
                            key.cancel();  // 从selectedKeys集合里移除key
                        } else {
                            split(buffer);
                            // 当buffer写满了都没有发现分隔符：\n，需要进行扩容
                            if(buffer.position() == buffer.limit()) {
                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2); // 每次扩容到2倍
                                buffer.flip();  // 旧的buffer切换到读取模式
                                newBuffer.put(buffer); // 将旧的buffer放入扩容后的新buffer里
                                key.attach(newBuffer);  // 最后将新buffer作为附件替换旧的buffer
                            }
                        }
                    }catch (Exception ex) {
                        ex.printStackTrace();
                        key.cancel();  // 当Client端被强制断开连接时，需要将key的事件取消，否则会一直循环不会被select()方法阻塞
                    }
                }

//                // 也可以不处理，直接cancel（cancel方法就是将key从selectedKeys集合里移除）
//                key.cancel();

                // 最后，selectedKeys集合里存放的是发生的事件，处理完成后必须从集合里移除，否则会有异常
                iterator.remove();
            }
        }

    }
}
